Function.RegisterNamespace("xUnit.js.Console");

xUnit.js.Console.Runner=function(){    
    // Private members
    var _count=0;
    var _durations;
    var _engine;
    var _exitCode;
    var _finished=false;
    var _globalState={};
    var _isStrict;
    var _isVerbose;
    var _knownPollution={};
    var _loadingDependency=false;
    var _output;
    var _scriptLoader;
    var _startTime;

    var _results={
        errors:[],
        failures:[],
        skipped:[],
        success:[],
        warnings:[]
    }

    this.Output=null;
    
    // ctor
    function Runner(){
        _durations=[];
        _exitCode=0;
        _scriptLoader=new xUnit.js.Console.ScriptLoader();
        _scriptLoader.Events.Add("Loading",ScriptLoader_Loading);
        _scriptLoader.Events.Add("Success",ScriptLoader_Success);
        _scriptLoader.Events.Add("Error",ScriptLoader_Error);
        this.Output=_output=new xUnit.js.Console.Output.OutputFormatter();
        _engine=xUnit.js.Attributes.Engine.Instance;
        _engine.Events.Add("AfterRun",Component_AfterRun);
        _engine.Events.Add("BeforeRun",Component_BeforeRun);
        System.IO.Path.DirectorySeparator=System.Environment.GetWorkingDirectory().indexOf('\\')>-1?'\\':'/';
    }
    if(this.constructor==xUnit.js.Console.Runner)Runner.apply(this,arguments);
    
    // IRunnable Members
    this.Run=function(){
        try{
            resetRun();
            var parameters=System.Environment.GetParameters();
            _isStrict=parameters.named.strict!="false";
            _isVerbose=parameters.named.verbose=="true";
            if(parameters.unnamed.length>0){
                loadDependency(parameters.named.dependency);
                _output.SetLevel(_output.OutputLevels[_isVerbose&&"verbose"]);
                _output.SetType(parameters.named.output);
                _output.Prologue();
                try{
                    setGlobalState();
                    _finished=false;
                    loadScripts(parameters.unnamed,parameters.named.extensions);
                    runAction(parameters.named.action,parameters.named.target||null,parameters.named.trait||null,parameters.named["-trait"]||null);
                    if(_count==0){
                        // SYNCHRONOUS FINISH
                        completeRun(true);
                    }
                    _finished=true;
                }catch(error){
                    _exitCode=0xBAE1; // BAIL: Runtime exception during script load or test run.
                    completeRun(true,String.Format("\nSystem Error:\n\t{0}\n",formatError(error)));
                }
            }else{
                usage();
            }
        }catch(criticalError){
            _exitCode=0xDEAD; // DEAD: Game over, man.
            completeRun(false,String.Format("\nCritical Error:\n\t{0}\n",formatError(criticalError)));
        }
    };

    // Private methods
    function collectState(){
        var bothersomeMembers=["setTimeout","clearTimeout"];
        return Object.GetKeys(Object.Global()).concat(bothersomeMembers);
    }    

    function completeRun(includeEpilogue,error){
        if(!error)_output.CompleteRun(_results.success,_results.failures,_results.errors,_results.warnings,_results.skipped,new Date()-_startTime);
        if(includeEpilogue)_output.Epilogue();
        if(error)System.Environment.WriteError(error);
        System.Environment.Exit(_exitCode);
    }

    function enumerateTests(target,trait,negativeTrait){
        Array.ForEach(_engine.Enumerate(target,trait,negativeTrait),enumerate);
    }
    
    function enumerate(model){
        _output.Enumerate(model);
    }
    
    function factCompleted(fact){
        var run={Component:fact,State:fact.State};
        switch(fact.State.Result){
            case xUnit.js.Model.Result.Error:
                _results.errors.push(run);
                _exitCode=0xBAD; // BAD: Your test is bad and you should feel bad.
                break;
            case xUnit.js.Model.Result.Failure:
                _results.failures.push(run);
                _exitCode=0xFAE1; // FAIL: Your test has failed. Get well soon.
                break;
            case xUnit.js.Model.Result.Skipped:
                _results.skipped.push(run);
                break;
            case xUnit.js.Model.Result.Success:
                var pollution=findPollution();
                if(pollution.length){
                    fact.State.Result=xUnit.js.Model.Result.Warning;
                    fact.State.Message=new xUnit.js.Model.Warning(String.Format("Global state pollution detected. Found new global variable{0}: {1}",pollution.length>1?'s':'',pollution.join(', ')));
                    return factCompleted(fact);
                }
                _results.success.push(run);
                break;
            case xUnit.js.Model.Result.Warning:
                _results.warnings.push(run);
                if(_isStrict){
                    _exitCode=0xB00; // WARN: Your test violates best practices. Stop it.
                }
                break;
        }
    }

    function findPollution(){
        var pollution=[];
        var currentState=collectState();
        for(var i=0;i<currentState.length;i++){
            var key=currentState[i];
            if(!_globalState[key]&&!_knownPollution[key]){
                if(isPollution(key)){
                    _knownPollution[key]=true;
                    pollution.push(key);
                }else{
                    _globalState[key]=true;
                }
            }
        }
        return pollution;
    }

    function formatError(error){
        return error&&error.toString(_isVerbose)||"[Unknown Error]";
    }

    function isPollution(key){
        var entry=Object.Global()[key];
        if(Function.IsNamespace(entry))return false;
        if(Object.IsType(Function,entry&&entry.GetModel)){
            var model=entry.GetModel();
            if(Object.IsType(xUnit.js.Model.Fixture,model)||Object.IsType(xUnit.js.Model.Fact,model))return false;
        }
        return true;
    }

    function loadDependency(dependency){
        if(dependency==undefined||String.IsEmpty(dependency))return;
        _loadingDependency=true;
        _scriptLoader.Events.Add("Loaded",ScriptLoader_DependencyLoaded);
        try{
            System.IO.Path.SetRoot(System.Environment.GetWorkingDirectory());
            var dependencies=_scriptLoader.GetScriptList(dependency.split(','));
            _scriptLoader.LoadScripts(dependencies);
        }catch(e){
            throw new Error(String.Format("Unable to load script dependencies: {0}",e.toString(_isVerbose)));
        }
    }

    function loadScripts(pathList,extensions){
        var timeStamp=new Date();
        _output.BeginFileLoad();
        try{
            System.IO.Path.SetRoot(System.Environment.GetWorkingDirectory());
            if(extensions)extensions=extensions.split(',');
            var scriptList=_scriptLoader.GetScriptList(pathList,extensions);
            _scriptLoader.LoadScripts(scriptList);
        }catch(e){
            throw new Error(String.Format("Unable to load scripts: {0}",e.toString(_isVerbose)));
        }
        _output.CompleteFileLoad(scriptList,new Date()-timeStamp);
    }

    function resetRun(){
        _durations.length=_results.success.length=_results.failures.length=_results.errors.length=_results.warnings.length=_results.skipped.length=0;
    }
    
    function runAction(action,target,trait,negativeTrait){
        if(!Object.IsType(String,action))action='';
        switch(action.toLowerCase()){
            case 'enumerate':
                enumerateTests(target,trait,negativeTrait);
                break;
            case 'runtests':
            default:
                runTests(target,trait,negativeTrait);
                break;
        }
    }
        
    function runTests(target,trait,negativeTrait){
        _startTime=new Date();
        _output.BeginRun();
        try{
            _engine.Run(target,trait,negativeTrait);
        }catch(e){
            throw e;
        }
    }
    
    function setGlobalState(){
        var keys=collectState();
        for(var i=0;i<keys.length;i++){
            _globalState[keys[i]]=true;
        }
    }

    function usage(){
        var message=[
            "",
            "xUnit.js Console Runner v0.8.3.9",
            "",
            "Usage:",
            "<environment> xUnit.js.Console.js [/action:<action>] [/extensions:<extensions>] [/output:<output>] [/strict:<strict>] [/target:<name>] [/-target:<name>] [/trait:<name>] [/-trait:<name>] [/verbose:<verbose>] [/dependency:<dependency>] <path>[ <path>...] ",
            "",
            "<environment> The environment in which the tests are to be run.",
            "",
            "              Supported environments include:",
            "",
            "                - Microsoft Script Engine (cscript.exe, Windows only)",
            "                - Google's V8 developer console (D8 executable)",
            "                - Mozilla's SpiderMonkey console (js executable)",
            "                - Mozilla's Rhino console (js.jar)",
            "",
            "<action>      The action to perform. Optional. Valid actions are ",
            "              [Enumerate|RunTests]. If omitted, defaults to 'RunTests'.",
            "",
            "<extensions>  A comma separated list of file extensions to include while",
            "              searching <path> for files to load. If omitted, defaults ", 
            "              to '.js'.",
            "",
            "<output>      The desired output type. Optional. Valid outputs are ",
            "              [Text|Xml|Json]. If omitted, defaults to 'Text'.",
            "",
            "<strict>      Toggles strict mode on or off. Valid options are [true|false].",
            "              Optional. If 'true', fails files and facts that violate ",
            "              Single-Assert or pollute the global namespace. If 'false', allows",
            "              multiple asserts per fact, and ignores global variables set in",
            "              files or facts. If omitted, defaults to 'true'.",
            "",
            "<target>      The name of a fact, fixture, or partial namespace to run. ",
            "              Optional. If omitted, runs all facts and fixtures found and ",
            "              registered at <path>.",
            "",
            "<trait>       The name, or comma separated list of names, of traits to run. ",
            "              Optional. If omitted, runs all facts and fixtures found and ",
            "              registered at <path>. If specified, runs only targets designated ",
            "              with the matching trait attribute, e.g. '[Trait(\"trait\")]'.",
            "",
            "<-trait>      The name, or comma separated list of names, of traits to skip. ",
            "              Optional. If omitted, runs all facts and fixtures found and ",
            "              registered at <path>. If specified, runs only targets not ",
            "              designated with a matching trait attribute.",
            "",
            "<verbose>     Toggles verbose mode on or off. Valid options are [true|false].",
            "              Optional. If 'true', includes additional information in output,",
            "              such as stack traces and file names. Defaults to false.",
            "",
            "<dependency>  The path or comma separated list of paths to a script file or ",
            "              directory containing dependencies to load before beginning the ",
            "              test run. These files will load before any files in <path>,",
            "              and before any output is written.",
            "",
            "<path>        The path or space separated list of paths to a script file or ",
            "              directory containing files to load and parse for tests.",
            "",
            "",
            "Examples:",
            "",
            "  cscript xUnit.js.Console.js /action:Enumerate xUnit.js/Tests",
            "",
            "  d8 - xUnit.js.Console.js -- /target:Test.xUnit.js.Console xUnit.js/Tests",
            "",
            "  js xUnit.js.Console.js xUnit.js/Tests/xUnit.js.Console/Runner.js",
            "",
            "  java -jar js.jar xUnit.js.Console.js /output:Xml xUnit.js/Tests",
            "",
            ""
        ];
        System.Environment.Write(message.join('\n'));
    }
    
    // Events
    function Component_AfterRun(context){
        if(Object.IsType(xUnit.js.Model.Fact,context.Component)){
            factCompleted(context.Component);
            _count--;
        }
        _output.CompleteComponent(context.Component,new Date()-_durations.pop());
        if(_finished&&_count==0){
            // ASYNCHRONOUS FINISH
            completeRun(true);
        }
    }

    function Component_BeforeRun(context){
        _durations.push(new Date());
        if(Object.IsType(xUnit.js.Model.Fact,context.Component))_count++;
        _output.BeginComponent(context.Component);
    }

    function ScriptLoader_DependencyLoaded(){
        _loadingDependency=false;
    }

    function ScriptLoader_Error(context){
        _exitCode=0x10AD; // LOAD: script failed to load.
        var errorMessage=String.Format("xUnit.js.Console.ScriptLoader.js: There was an error loading script {2}'{0}'.\nError: {1}\n",context.Path,context.Error.toString(_isVerbose),_loadingDependency?"dependency ":'');
        if(_loadingDependency)throw new Error(errorMessage);
        _output.Error(new Error(errorMessage));
    }

    function ScriptLoader_Loading(context){
        _engine.CurrentFile=context.Path;
    }

    function ScriptLoader_Success(context){
        if(_loadingDependency)return;
        var pollution=findPollution();
        if(pollution.length){
            _results.warnings.push({Component:{File:context.Path,GetPath:function(){return "Global.Runtime"}},State:{Result:xUnit.js.Model.Result.Warning,Message:new xUnit.js.Model.Warning(String.Format("Global state pollution detected while loading '{0}'. Found new global variable{1}: {2}",context.Path,pollution.length>1?'s':'',pollution.join(', ')))}});
        }
        _output.FileLoadSuccess(context.Path,context.Duration);
    }
};

xUnit.js.Console.Runner.Implement(xUnit.js.IRunnable,'xUnit.js.Console.Runner');